// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.core.ws.runtime.xml;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.util.Calendar;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import com.microsoft.tfs.core.ws.runtime.Schemas;
import com.microsoft.tfs.core.ws.runtime.types.GUID;
import com.microsoft.tfs.util.base64.Base64;

/**
 * Static helper methods for XML serialization via StAX. These methods will be
 * called from beans generated by com.microsoft.tfs.core.ws.generator.
 *
 * @threadsafety thread-compatible
 */
public abstract class XMLStreamWriterHelper {
    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final String value)
        throws XMLStreamException {
        if (value == null) {
            return;
        }

        writer.writeAttribute(attributeName, value);
    }

    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @param includeTime
     *        if <code>true</code> the time information in the {@link Calendar}
     *        is included in the string (an XML Schema "DateTime" value is
     *        written), if <code>false</code> the time information is omitted
     *        from the string (an XML Schema "Date" value is written)
     * @throws XMLStreamException
     */
    public static void writeAttribute(
        final XMLStreamWriter writer,
        final String attributeName,
        final Calendar value,
        final boolean includeTime) throws XMLStreamException {
        if (value == null) {
            return;
        }

        writer.writeAttribute(attributeName, XMLConvert.toString(value, includeTime));
    }

    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final short value)
        throws XMLStreamException {
        writer.writeAttribute(attributeName, XMLConvert.toString(value));
    }

    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final int value)
        throws XMLStreamException {
        writer.writeAttribute(attributeName, XMLConvert.toString(value));
    }

    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final long value)
        throws XMLStreamException {
        writer.writeAttribute(attributeName, XMLConvert.toString(value));
    }

    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final float value)
        throws XMLStreamException {
        writer.writeAttribute(attributeName, XMLConvert.toString(value));
    }

    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final byte[] value)
        throws XMLStreamException {
        if (value == null) {
            return;
        }

        try {
            writer.writeAttribute(attributeName, new String(Base64.encodeBase64(value), "US-ASCII")); //$NON-NLS-1$
        } catch (final UnsupportedEncodingException e) {
            /*
             * Should never happen because "US-ASCII" is required by all Java
             * implementations.
             */
            throw new RuntimeException(e);
        }
    }

    /**
     * Writes an attribute with the given name for each string in the given
     * array.
     *
     * @todo Remove? Is this used or correct for TFS?
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final String[] value)
        throws XMLStreamException {
        if (value == null || value.length == 0) {
            return;
        }

        for (int i = 0; i < value.length; i++) {
            writer.writeAttribute(attributeName, value[i]);
        }
    }

    /**
     * Writes an attribute.
     *
     * @param writer
     *        the writer (not null).
     * @param attributeName
     *        the name of the attribute to write (not null).
     * @param value
     *        the value of the attribute to write (if null, attribute is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeAttribute(final XMLStreamWriter writer, final String attributeName, final boolean value)
        throws XMLStreamException {
        writer.writeAttribute(attributeName, XMLConvert.toString(value));
    }

    /**
     * Writes an element.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write (if null, element is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeElement(final XMLStreamWriter writer, final String elementName, final String value)
        throws XMLStreamException {
        if (value == null) {
            return;
        }

        writer.writeStartElement(elementName);
        writer.writeCharacters(value);
        writer.writeEndElement();
    }

    /**
     * Writes an element.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write (if null, element is not
     *        written).
     * @param includeTime
     *        if <code>true</code> the time information in the {@link Calendar}
     *        is included in the string (an XML Schema "DateTime" value is
     *        written), if <code>false</code> the time information is omitted
     *        from the string (an XML Schema "Date" value is written)
     * @throws XMLStreamException
     */
    public static void writeElement(
        final XMLStreamWriter writer,
        final String elementName,
        final Calendar value,
        final boolean includeTime) throws XMLStreamException {
        if (value == null) {
            return;
        }

        writer.writeStartElement(elementName);
        writer.writeCharacters(XMLConvert.toString(value, includeTime));
        writer.writeEndElement();
    }

    /**
     * Writes an element.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write.
     * @throws XMLStreamException
     */
    public static void writeElement(final XMLStreamWriter writer, final String elementName, final int value)
        throws XMLStreamException {
        writer.writeStartElement(elementName);
        writer.writeCharacters(XMLConvert.toString(value));
        writer.writeEndElement();
    }

    /**
     * Writes an element.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write.
     * @throws XMLStreamException
     */
    public static void writeElement(final XMLStreamWriter writer, final String elementName, final long value)
        throws XMLStreamException {
        writer.writeStartElement(elementName);
        writer.writeCharacters(XMLConvert.toString(value));
        writer.writeEndElement();
    }

    /**
     * Writes an element with the given name for each value in the given array.
     *
     * @todo Remove? Is this used or correct for TFS?
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write (if null, element is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeElement(final XMLStreamWriter writer, final String elementName, final String[] value)
        throws XMLStreamException {
        if (value == null || value.length == 0) {
            return;
        }

        writer.writeStartElement(elementName);
        for (int i = 0; i < value.length; i++) {
            writer.writeStartElement("string"); //$NON-NLS-1$
            writer.writeCharacters(value[i]);
            writer.writeEndElement();
        }
        writer.writeEndElement();
    }

    /**
     * Writes an element.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write (if null, element is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeElement(final XMLStreamWriter writer, final String elementName, final byte[] value)
        throws XMLStreamException {
        if (value == null) {
            return;
        }

        writer.writeStartElement(elementName);
        try {
            writer.writeCharacters(new String(Base64.encodeBase64(value), "US-ASCII")); //$NON-NLS-1$
        } catch (final UnsupportedEncodingException e) {
            /*
             * Should never happen because "US-ASCII" is required by all Java
             * implementations.
             */
            throw new RuntimeException(e);
        }
        writer.writeEndElement();
    }

    /**
     * Writes an element.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write (if null, element is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeElement(final XMLStreamWriter writer, final String elementName, final boolean value)
        throws XMLStreamException {
        writer.writeStartElement(elementName);
        writer.writeCharacters(XMLConvert.toString(value));
        writer.writeEndElement();
    }

    /**
     * Writes an {@link Object} as an element, supporting most Java object types
     * which map to XSD simple types (Integer, Short, Byte, Boolealn, Calendar,
     * etc.), and also arrays of those types, and arrays of arrays, etc.
     * <p>
     * Not all distinct serializable types are supported. For example, a
     * {@link Calendar} is always serialized as an XML Schema "dateTime", not as
     * a "date". Visual Studio has the same behavior.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write (not null).
     * @param value
     *        the value of the element to write (if null, element is not
     *        written).
     * @throws XMLStreamException
     */
    public static void writeElement(final XMLStreamWriter writer, final String elementName, final Object value)
        throws XMLStreamException {
        if (value == null) {
            /*
             * Turn on the "nil" attribute.
             */
            writer.writeStartElement(elementName);
            writer.writeAttribute(Schemas.XSI, "nil", "true"); //$NON-NLS-1$ //$NON-NLS-2$
            writer.writeEndElement();
        } else {
            String localName;
            String namespace;
            String stringValue;

            if (value instanceof Boolean) {
                localName = "boolean"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Boolean) value).booleanValue());
                namespace = Schemas.XSD;
            } else if (value instanceof Byte) {
                /*
                 * Note how we encode Byte as an unsignedByte, even though
                 * Java's Byte (and byte) are signed. This is the most natural
                 * mapping between Java Byte and .NET Byte (which is unsigned),
                 * because it does not require a new UnsignedByte class or
                 * similar, and most Java programmers are familiar with the
                 * quirks of signed bytes. Microsoft seems to use only the
                 * unsignedByte on the wire for this kind of object
                 * serialization, never the signed byte type.
                 *
                 * Notice the use of shortValue() to get the value unsigned.
                 */
                localName = "unsignedByte"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Byte) value).shortValue());
                namespace = Schemas.XSD;
            } else if (value instanceof Character) {
                localName = "char"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Character) value).charValue());
                namespace = Schemas.MICROSOFT_WSDL;
            } else if (value instanceof Short) {
                localName = "short"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Short) value).shortValue());
                namespace = Schemas.XSD;
            } else if (value instanceof Integer) {
                localName = "int"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Integer) value).intValue());
                namespace = Schemas.XSD;
            } else if (value instanceof Long) {
                localName = "long"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Long) value).longValue());
                namespace = Schemas.XSD;
            } else if (value instanceof Float) {
                localName = "float"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Float) value).floatValue());
                namespace = Schemas.XSD;
            } else if (value instanceof Double) {
                localName = "double"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Double) value).doubleValue());
                namespace = Schemas.XSD;
            } else if (value instanceof Float) {
                localName = "float"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Float) value).floatValue());
                namespace = Schemas.XSD;
            } else if (value instanceof String) {
                localName = "string"; //$NON-NLS-1$
                stringValue = (String) value;
                namespace = Schemas.XSD;
            } else if (value instanceof Calendar) {
                localName = "dateTime"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((Calendar) value), true);
                namespace = Schemas.XSD;
            } else if (value instanceof BigDecimal) {
                localName = "decimal"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((BigDecimal) value));
                namespace = Schemas.XSD;
            } else if (value instanceof GUID) {
                localName = "guid"; //$NON-NLS-1$
                stringValue = XMLConvert.toString(((GUID) value));
                namespace = Schemas.MICROSOFT_WSDL;
            } else if (value instanceof byte[]) {
                localName = "base64Binary"; //$NON-NLS-1$
                try {
                    stringValue = new String(Base64.encodeBase64((byte[]) value), "US-ASCII"); //$NON-NLS-1$
                } catch (final UnsupportedEncodingException e) {
                    /*
                     * Should never happen because "US-ASCII" is required by all
                     * Java implementations.
                     */
                    throw new RuntimeException(e);
                }
                namespace = Schemas.XSD;
            } else {
                if (value.getClass().isArray()) {
                    writer.writeStartElement(elementName);
                    writer.writeAttribute(Schemas.XSI, "type", "ArrayOfAnyType"); //$NON-NLS-1$ //$NON-NLS-2$
                    XMLStreamWriterHelper.writeObjectArray(writer, null, (Object[]) value);
                    writer.writeEndElement();
                }

                /*
                 * Return early because the full array was written, or it wasn't
                 * an array and we can't write anything.
                 */
                return;
            }

            /*
             * Write out the value as an element with the correct type
             * attribute.
             */
            writer.writeStartElement(elementName);
            writer.writeAttribute(
                Schemas.XSI,
                "type", //$NON-NLS-1$
                writer.getNamespaceContext().getPrefix(namespace) + ":" + localName); //$NON-NLS-1$
            writer.writeCharacters(stringValue);
            writer.writeEndElement();
        }

    }

    /**
     * Writes an array of {@link Object}s to the stream, possibly recursively
     * calling this method or
     * {@link #writeElement(XMLStreamWriter, String, Object)} to handle items.
     *
     * @param writer
     *        the writer (not null).
     * @param elementName
     *        the name of the element to write, if null or empty no outer
     *        element is written, only the array contents are written as
     *        elements.
     * @param value
     *        the value of the element to write (if null, element is not
     *        written).
     * @throws XMLStreamException
     */
    private static void writeObjectArray(final XMLStreamWriter writer, final String elementName, final Object[] value)
        throws XMLStreamException {
        if (value != null && value.length != 0) {
            if (elementName != null && elementName.length() > 0) {
                writer.writeStartElement(elementName);
            }

            for (int i = 0; i < value.length; i++) {
                if (value[i] == null) {
                    throw new IllegalArgumentException("value[" + i + "]"); //$NON-NLS-1$ //$NON-NLS-2$
                }

                XMLStreamWriterHelper.writeElement(writer, "anyType", value[i]); //$NON-NLS-1$
            }

            if (elementName != null && elementName.length() > 0) {
                writer.writeEndElement();
            }
        }
    }

    /**
     * Turns newline characters (\r, \n) in the given text into XML entities in
     * the returned text so they will be preserved when interpreted as an XML
     * attribute value.
     *
     * @param text
     *        the input text to escape all newlines in. If null, null is
     *        returned.
     * @return the input text with all newlines transformed into XML entities,
     *         or null if null was given.
     */
    public static String escapeNewlinesForXMLAttribute(final String text) {
        if (text == null) {
            return null;
        }

        final StringBuffer sb = new StringBuffer();

        final int length = text.length();
        for (int i = 0; i < length; i++) {
            final char c = text.charAt(i);

            if (c == '\r') {
                sb.append("&#xD;"); //$NON-NLS-1$
            } else if (c == '\n') {
                sb.append("&#xA;"); //$NON-NLS-1$
            } else {
                sb.append(c);
            }
        }

        return sb.toString();
    }
}
